package com.zhang.sdk.utils.log;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.util.SparseArray;

import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;

import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.regex.Pattern;
/**
 * 保存错误日志的工具类
 *  
 *
 * @author ShiPengHao
 * @date 2017/12/26
 * <p>
 * 修改信息：增加文件路径和文件名
 * updateTime 2019/07/16
 */
@SuppressWarnings({"ResultOfMethodCallIgnored", "SameParameterValue"})
public class ErrorLogUtil {
    /**
     * 本类名称
     */
    public static final String CLASS_NAME = ErrorLogUtil.class.getName();
    /**
     * 堆栈信息最大深度：函数引用的depth是1, 调用者的depth是2,调用者的调用者是3
     */
    public static final int MAX_STACK_DEPTH = 5;
    /**
     * 新的文件名后缀
     */
    private static final String NEW_FILE_SUFFIX = ".txt";
    /**
     * SD最小可用值（低于此值不再写入）
     */
    private static final long MIN_AVAILABLE_VALUE = 1024 * 1024L;
    /**
     * 日志文件名称格式，使用日期作为文件名称
     */
    private static final SimpleDateFormat LOG_FILE_NAME_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA);
    /**
     * 日志文件中的时间标签的格式
     */
    private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.CHINA);

    /**
     * 清除日志的模式：清除非本月
     */
    private static final int CLEAR_SCOPE_MONTH = 1;
    /**
     * 清除日志的模式：清除N天以前
     */
    public static final int CLEAR_SCOPE_DAY_OFFSET = 2;

    /**
     * 消息类型限定范围
     */
    @IntDef({CLEAR_SCOPE_MONTH, CLEAR_SCOPE_DAY_OFFSET})
    @Retention(RetentionPolicy.SOURCE)
    @Documented
    @interface LogClearScope {
    }

    /**
     * 错误类型：无网络
     */
    public static final int ERROR_TYPE_NETWORK = 1;
    /**
     * 错误类型：网络链接异常，一般是服务端错误
     */
    public static final int ERROR_TYPE_SERVICE_ERROR = 2;
    /**
     * 错误类型：接口返回信息为空，一般是json解析失败
     */
    public static final int ERROR_TYPE_RESPONSE_EMPTY = 3;
    /**
     * 错误类型：接口返回错误信息
     */
    public static final int ERROR_TYPE_RESPONSE_ERROR = 4;

    /**
     * 错误类型
     */
    @IntDef({ERROR_TYPE_NETWORK, ERROR_TYPE_SERVICE_ERROR, ERROR_TYPE_RESPONSE_EMPTY, ERROR_TYPE_RESPONSE_ERROR})
    @Retention(RetentionPolicy.SOURCE)
    @Documented
    @interface ErrorType {
    }

    /**
     * 错误日志文件夹全路径
     */
    public static String sLogDirName;
    /**
     * app信息
     */
    public static String sAppInfo;
    /**
     * 设备信息
     */
    public static String sDeviceInfo;
    /**
     * 存储空间不足
     */
    public static volatile boolean sPoorStorage;

    /**
     * 保存常见错误信息键值对的数据集合，这里可以根据实际需要动态添加
     */
    public static final SparseArray<String> COMMON_ERROR_PAIR = new SparseArray<>();

    static {
        COMMON_ERROR_PAIR.put(ERROR_TYPE_NETWORK, "无网络");
        COMMON_ERROR_PAIR.put(ERROR_TYPE_SERVICE_ERROR, "网络异常");
        COMMON_ERROR_PAIR.put(ERROR_TYPE_RESPONSE_EMPTY, "接口返回信息为空");
        COMMON_ERROR_PAIR.put(ERROR_TYPE_RESPONSE_ERROR, "接口返回错误信息");
    }

    /**
     * 初始化，应在应用初始化方法中调用
     *
     * @param context ctx
     */
    public synchronized static void init(Context context, String path) {
        if (null == sLogDirName) {
            // 应用的外置存储目录 外置存储(sdcard)/Android/data/包名
            File file = context.getExternalFilesDir(path);
            if (file == null || !file.exists()) {
                file.mkdirs();
            }
            sLogDirName = file.getAbsolutePath();
            setAPPInfo(context);
            setDeviceInfo();
        }
    }

    /**
     * 检查log写入环境是否具备条件：初始化 + sd卡挂载 + 文件夹路径创建成功
     *
     * @return 具备条件true，否则false
     */
    private synchronized static boolean checkEnvironment() {
        return Environment.MEDIA_MOUNTED.equalsIgnoreCase(Environment.getExternalStorageState());
    }


    /**
     * 新的（包含路径和文件名称）保存错误日志的快捷方法，根据错误码自动匹配常见错误信息并保存
     *
     * @param errorType   错误类型
     * @param logDir      日志文件所在目录
     * @param logFileName 日志文件名
     */
    public static void saveError(Context context, @ErrorType int errorType, @NonNull String logDir,
                                 @NonNull String logFileName) {
        saveError(context, COMMON_ERROR_PAIR.get(errorType), true, logDir, logFileName);
    }

    /**
     * 新的（包含路径和文件名称）保存错误日志
     *
     * @param metaData    原始错误信息
     * @param trace       是否生成当前程序运行点的堆栈信息并保存到日志中
     * @param logDir      日志文件所在目录
     * @param logFileName 日志文件名
     */
    private static void saveError(Context context, String metaData, boolean trace,
                                  @NonNull String logDir, @NonNull String logFileName) {
        try {
            if (checkEnvironment()) {
//                if (!checkPath(logDir)) {
//                    LogUtil.e("路径:" + logDir + ", 不是正确的路径！");
//                    throw new RuntimeException("路径:" + logDir + ", 不是正确的路径！");
//                }
                initDir(context, true, logDir);

                String log = generateLog(metaData, trace);
                if (!logFileName.endsWith(NEW_FILE_SUFFIX)) {
                    logFileName += NEW_FILE_SUFFIX;
                }
                writeLog2File(logFileName, log);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化日志存储路径
     *
     * @param context 上下文
     * @param isNew   是否是新目录
     * @param logDir  日志目录
     */
    private static void initDir(Context context, boolean isNew, @NonNull String logDir) {
        File file;
        if (isNew) {
            file = new File(logDir);
        } else {
            file = context.getExternalFilesDir(logDir);
        }

        if (file == null || !file.exists()) {
            file.mkdirs();
        }
        sLogDirName = file.getAbsolutePath();
    }

    /**
     * 正则路径
     */
    private static final Pattern PATTERN = Pattern.compile("^\\/(\\w+\\/?)+$");

    /**
     * 检测是否是正确的路径
     *
     * @param logDir 路径
     * @return true是正确的路径，否则不是
     */
    private static boolean checkPath(String logDir) {
        return PATTERN.matcher(logDir).matches();
    }

    /**
     * 新的（包含路径和文件名称）保存异常信息
     *
     * @param ex          异常
     * @param logDir      日志文件所在目录
     * @param logFileName 日志文件名
     */
    public static void saveException(Context context, Throwable ex, @NonNull String logDir,
                                     @NonNull String logFileName) {
        saveError(context, parseThrowable2String(ex), false, logDir, logFileName);
    }

    /**
     * 新的（包含路径和文件名称）保存异常信息
     *
     * @param exStr       异常信息字符串
     * @param logDir      日志文件所在目录
     * @param logFileName 日志文件名
     */
    public static void saveException(Context context, String exStr, @NonNull String logDir,
                                     @NonNull String logFileName) {
        saveError(context, exStr, false, logDir, logFileName);
    }

    /**
     * 清除非当月日志文件，并把删除记录写入当前日志文件中
     */
    public static void clear(Context context) {
        clear(context, CLEAR_SCOPE_MONTH, 10);
    }

    /**
     * 清除日志文件，并把删除记录写入当前日志文件中
     *
     * @param scope   清除模式
     * @param dayKeep dayKeep天以内的日志不清除，只有在模式为{@link #CLEAR_SCOPE_DAY_OFFSET}时有效
     */
    public static void clear(Context context, @LogClearScope int scope, @IntRange(from = 1, to = 15) int dayKeep) {
        try {
            if (checkEnvironment()) {
                File dir = new File(sLogDirName);
                // 获取非本月的日志文件集合
                FilenameFilter filter;
                switch (scope) {
                    case CLEAR_SCOPE_MONTH:
                        filter = new FileFilterByMonth();
                        break;
                    case CLEAR_SCOPE_DAY_OFFSET:
                        filter = new FileFilterByDayOffset(dayKeep);
                        break;
                    default:
                        filter = new FileFilterByMonth();
                        break;
                }
                File[] files = dir.listFiles(filter);
                // 遍历集合，删除日志文件，并写入当前日志
                for (File file : files) {
                    if (file.isFile() && file.delete()) {
                        saveError(context, String.format("删除了日志文件：%s", file.getName()), false,file.getName(), file.getName());
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 将拼接好的日志信息写入文件，文件是日期名称的txt文件
     *
     * @param log 日志
     */
    private static void writeLog2File(String logFileName, String log) {
        File externalStorageDirectory = Environment.getExternalStorageDirectory();
        long usableSpace = externalStorageDirectory.getUsableSpace();
        if (usableSpace < MIN_AVAILABLE_VALUE) {
            // 存储不足1M时，不再写入日志
            if (sPoorStorage) {
                return;
            } else {
                log = "time:" + TIME_FORMAT.format(new Date()) + "\tSD卡存储空间不足，不再写入日志文件";
                sPoorStorage = true;
            }
        } else {
            if (sPoorStorage) {
                sPoorStorage = false;
            }
        }
        File file = new File(sLogDirName, logFileName);
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter(file, true);
            fileWriter.write(log);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileWriter != null) {
                    fileWriter.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 生成错误日志
     *
     * @param errorMsg 原始错误信息
     * @param trace    是否生成堆栈信息并保存
     * @return 错误日志字符串
     */
    public static String generateLog(String errorMsg, boolean trace) {
        // head:start flag + time + error message
        StringBuilder result = new StringBuilder("****************************************************>>>>\ntime:\t")
                .append(TIME_FORMAT.format(new Date()))
                .append("\nmessage:\n\t")
                .append(errorMsg)
                .append("\n");
        // stack track
        if (trace) {
            appendStackTrace(result);
        }
        // APP
        result.append(sAppInfo);
        // device
        result.append(sDeviceInfo);
        // tail:end flag
        result.append("<<<<****************************************************\n\n");
        return result.toString();
    }

    /**
     * 获取设备硬件信息，保存到静态成员变量中
     */
    public static void setDeviceInfo() {
        sDeviceInfo = "device:" +
                "\n\tMANUFACTURER = " + Build.MANUFACTURER +// 制造商
                "\n\tMODEL = " + Build.MODEL +// 型号
                "\n\tDISPLAY = " + Build.DISPLAY +//版本号
                "\n\tCPU_ABI = " + Build.CPU_ABI +//CPU
                "\n\tCPU_ABI2 = " + Build.CPU_ABI2 +//CPU2
                "\n\tHARDWARE = " + Build.HARDWARE +// 硬件名称
                "\n\tDEVICE = " + Build.DEVICE +// 驱动名称
                "\n\tVERSION.RELEASE = " + Build.VERSION.RELEASE +// android 版本号
                "\n\tVERSION.SDK_INT = " + Build.VERSION.SDK_INT +// android sdk 级别
                "\n";
    }

    /**
     * 获取APP应用信息，保存到静态成员变量中
     *
     * @param context ctx
     */
    public static void setAPPInfo(Context context) {
        StringBuilder result = new StringBuilder("APP:");
        try {
            PackageManager pm = context.getPackageManager();
            PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
            result.append("\n\tpackageName=").append(context.getPackageName())
                    .append("\n\tversionName=").append(pi.versionName)
                    .append("\n\tversionCode=").append(pi.versionCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
        result.append("\n");
        sAppInfo = result.toString();
    }

    /**
     * 添加拼接堆栈信息到错误信息中
     *
     * @param result 错误信息StringBuilder
     */
    public static void appendStackTrace(StringBuilder result) {
        result.append("stack trace:");
        StackTraceElement[] stackTraceElements = new Throwable().getStackTrace();
        StackTraceElement element;
        // 这里+3是因为此方法、generateLog、saveErrorLog方法占用了3层堆栈信息
        final int depth = Math.min(MAX_STACK_DEPTH + 3, stackTraceElements.length);
        for (int i = 3; i < depth; i++) {
            element = stackTraceElements[i];
            if (null == element || CLASS_NAME.equalsIgnoreCase(element.getClassName())) {
                continue;
            }
            // 格式： at android.os.Looper.loop(Looper.java:194)
            result.append("\n\tat ")
                    .append(element.getClassName())
                    .append(".")
                    .append(element.getMethodName())
                    .append("(")
                    .append(element.getFileName())
                    .append(":line ")
                    .append(element.getLineNumber())
                    .append(")");
        }
        result.append("\n");
    }

    /**
     * 将异常解析为字符串信息
     *
     * @param ex 异常
     * @return 字符串信息
     */
    public static String parseThrowable2String(Throwable ex) {
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        ex.printStackTrace(printWriter);
        Throwable cause = ex.getCause();
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }
        printWriter.close();
        String result = writer.toString();
        try {
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 根据日志文件名称（如2017-12-26.txt）的月份信息，来筛选非本月的日志文件
     */
    public static class FileFilterByMonth implements FilenameFilter {
        /**
         * 当前月
         */
        public final int MONTH_NOW = Calendar.getInstance().get(Calendar.MONTH);

        @Override
        public boolean accept(File dir, String filename) {
            filename = filename.substring(0, filename.lastIndexOf("."));
            try {
                Date fileDate = LOG_FILE_NAME_FORMAT.parse(filename);
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(fileDate);
                return MONTH_NOW != calendar.get(Calendar.MONTH);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return false;
        }
    }

    /**
     * 根据日志文件名称（如2017-12-26.txt）的日期ms值和需要保存的日志的天数，筛选出需要删除的日志文件
     */
    public static class FileFilterByDayOffset implements FilenameFilter {
        /**
         * 偏移ms
         */
        public final long mTimeOffset;
        /**
         * 当前ms
         */
        public final long mTimeToday = System.currentTimeMillis();

        /**
         * 构造
         *
         * @param dayOffset 偏移天数
         */
        public FileFilterByDayOffset(int dayOffset) {
            mTimeOffset = dayOffset * 24 * 60 * 60 * 1000;
        }

        @Override
        public boolean accept(File dir, String filename) {
            filename = filename.substring(0, filename.lastIndexOf("."));
            try {
                // 文件日期，ms
                long fileTime = LOG_FILE_NAME_FORMAT.parse(filename).getTime();
                return (mTimeToday - fileTime) > mTimeOffset;
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return false;
        }
    }
}
